/**
 *  Copyright 2013 Nate Dierk
 *
 *  COMP 590 -- Fall 2013
 */

#include "GContext.h"
#include "GBitmap.h"
#include "GColor.h"
#include "GIRect.h"

 #include <algorithm>

 #define GPIXEL_SCALE_FACTOR 255

///////////////////////////////////////////////////////////
// Functions that were defined in other files, now as local helpers

 enum BlendType{
  SET,       // ignores and overwrites any existing values: dst = src
  SRC_OVER,  // source over blending: dst = src + (1 - src.fA)*dst
};

static inline unsigned GPixel_CompPreMul(float alpha, float c){
  return (unsigned) (alpha * c * GPIXEL_SCALE_FACTOR);
}

// premul and pack into pixel
static inline GPixel GPixel_PackColor(const GColor& color){
  return GPixel_PackARGB( 
    GPixel_CompPreMul(    1.0f, color.fA), //alpha is special, doesn't pre-mul itself
    GPixel_CompPreMul(color.fA, color.fR), 
    GPixel_CompPreMul(color.fA, color.fG), 
    GPixel_CompPreMul(color.fA, color.fB)
    ); 
}

static inline void GPixel_Scale(GPixel* pix, unsigned factor){
  GASSERT(factor <= 255);

  uint32_t a, r, g, b;
  a = ((GPixel_GetA(*pix) * factor) + 255) >> 8;
  r = ((GPixel_GetR(*pix) * factor) + 255) >> 8;
  g = ((GPixel_GetG(*pix) * factor) + 255) >> 8;
  b = ((GPixel_GetB(*pix) * factor) + 255) >> 8;

  *pix = GPixel_PackARGB(a, r, g, b);
}

static inline void GPixel_Blend(GPixel* dst, const GPixel& src, 
                                const BlendType blend = SRC_OVER, 
                                float alpha = 1.0f){
  GPixel copy = src; // copies so I can safely edit alpha

  if (alpha != 1.0f){ // apply external alpha
    GPixel_Scale(&copy, alpha * GPIXEL_SCALE_FACTOR);
  }

  unsigned sA = GPixel_GetA(copy);

  if (GPIXEL_SCALE_FACTOR == sA
    || SET == blend) {
    *dst = copy;
  } else if (SRC_OVER == blend){
    GPixel_Scale(dst, (GPIXEL_SCALE_FACTOR - sA));
    *dst = copy + *dst;
  }
};

 static GIRect GIRect_ClipToBmp(const GIRect& rect, const GBitmap& bmp) {
  int32_t L, T, R, B;
  L = std::max(0, rect.fLeft);
  T = std::max(0, rect.fTop);
  R = std::min(rect.fRight, bmp.fWidth);
  B = std::min(rect.fBottom, bmp.fHeight);

  return GIRect::MakeLTRB(L, T, R, B);
}

static void GBitmap_BlendPixels(GBitmap* bmp, int w, int h, char* dst, 
                                const GPixel& src, BlendType blend = SRC_OVER){
    int x, y;
    for (y = 0; y < h; ++y){
        for (x = 0; x < w; ++x){
          GPixel_Blend( &((GPixel*) dst)[x], src, blend);
      }
      dst += bmp->fRowBytes;
  }
}

static void GBitmap_ApplyAlpha(GBitmap* bmp, float a){
    GASSERT(a >= 0.0f && a <= 1.0f);

    if (1.0f != a){
        const GPixel alphaPixel = GPixel_PackColor(GColor::Make(1.0f - a, 0.0f, 0.0f, 0.0f));
        GBitmap_BlendPixels(bmp, bmp->fWidth, bmp->fHeight, (char*) bmp->fPixels, alphaPixel);
    }
}

///////////////////////////////////////////////////////////
// GContext0 Definition

class GContext0 : public GContext {
protected:
  GBitmap fBmp;

public:
    GContext0(const GBitmap& bmp) {
      fBmp = bmp;
    }
    virtual ~GContext0() {}

    // *  Return the information about the context's bitmap
    void getBitmap(GBitmap* bmp) const {
      *bmp = fBmp;
    }

  // *  Set the entire context's pixels to the specified value.
  void clear(const GColor& color){
    char* row = (char*) fBmp.fPixels;    

    GBitmap_BlendPixels(&fBmp, fBmp.fWidth, fBmp.fHeight, row,
                GPixel_PackColor(color), SET);
  };

  void fillIRect(const GIRect& rect, const GColor& color){
    GIRect clipped = GIRect_ClipToBmp(rect, fBmp);

    if (!clipped.isEmpty()){ 
      char* row = ((char*) (fBmp.fPixels + clipped.fLeft)) + // compute pointer to top left pixel
                  (fBmp.fRowBytes * clipped.fTop); // add vertical offset

      GBitmap_BlendPixels(&fBmp, clipped.width(), clipped.height(), row, GPixel_PackColor(color));
    }
  };

  void drawBitmap(const GBitmap& bmp, int x, int y, float alpha = 1.0f){
    GIRect rect = GIRect::MakeXYWH(x, y, bmp.fWidth, bmp.fHeight);
    GIRect clipped = GIRect_ClipToBmp(rect, fBmp);

    if (!clipped.isEmpty()){
      // blend with context's bitmap
      char* dst = ((char*) (fBmp.fPixels + x)) + // compute pointer to top left pixel
                  (fBmp.fRowBytes * y); // add vertical offset
      char* src = (char*) bmp.fPixels;

      int i, j;
      for (i = 0; i < clipped.height(); ++i){
        for (j = 0; j < clipped.width(); ++j){
          GPixel_Blend( &((GPixel*) dst)[j], ((GPixel*) src)[j], SRC_OVER, alpha );
        }
        src += bmp.fRowBytes;
        dst += fBmp.fRowBytes;
      }
    }
  };
};



///////////////////////////////////////////////////////////
// Specialized GContext Implementations

class GContext_bmp : public GContext0 {
public:
  GContext_bmp(const GBitmap& bmp) : GContext0(bmp) { }
  ~GContext_bmp(){  }
};

class GContext_hw : public GContext0 {
public:  
  GContext_hw(const GBitmap& bmp) : GContext0(bmp) {}
  ~GContext_hw(){
    free(fBmp.fPixels); 
  }
};



///////////////////////////////////////////////////////////
// Context Validation Helpers
  
static bool isBmpValid(const GBitmap& bmp){
  return  !(bmp.fHeight   <= 0 || 
	    bmp.fWidth    <= 0 || 
	    bmp.fRowBytes < (bmp.fWidth * sizeof(GPixel)));
}

static bool isMemValid(const GBitmap& bmp){
  return bmp.fPixels != NULL;
}



///////////////////////////////////////////////////////////
// GContext Factories

GContext* GContext::Create(const GBitmap& bmp){
  if (isBmpValid(bmp) && isMemValid(bmp)){
    return new GContext_bmp(bmp);
  }
  return NULL;
}

GContext* GContext::Create(int width, int height){
  GBitmap bmp;
  bmp.fWidth = width;
  bmp.fHeight = height;
  bmp.fRowBytes = width * sizeof(GPixel);

  if (isBmpValid(bmp)){
    bmp.fPixels = (GPixel*) malloc(height * bmp.fRowBytes);
    
    if (isMemValid(bmp)){
      const GBitmap& pBmp = bmp;
      return new GContext_hw(pBmp);
    }
  } 
  return NULL;
}